//============================================================================
// UsbLink.cs
//
// Copyright (c) 2005-2006 U.S. TrailMaps, LLC
// THIS MATERIAL IS CONFIDENTIAL AND PROPRIETARY TO U.S. TRAILMAPS AND
// MAY NOT BE REPRODUCED, PUBLISHED OR DISCLOSED TO OTHERS WITHOUT COMPANY
// AUTHORIZATION.
//
// This work is derived from Waymex GPS Library for .Net version 2.7.9 source.
// Portions copyright (c) 2004-2005 Waymex IT Ltd
//============================================================================

// Turn this define on to emulate a USB GPS device. This is very useful for diagnosing a customer problem given a GPS trace log file.
// Set up the device responses to match the responses from the trace file in the UsbLinkWorkerThread.DoTransmit method.
//#define EMULATE_DEVICE

using System;
using System.Collections;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Threading;
using Waymex.Diagnostics;
using Waymex.Gps;

namespace Waymex.Gps.Garmin.Usb
{
	internal class UsbLink : LinkBase
	{
        // error constants
        private const string ERR_9000_MSG = "GPS Device not detected on Universal Serial Bus.";
        //private const string ERR_9001_MSG = "Unable to obtain handle to USB Device.";
        //private const string ERR_9002_MSG = "Unable to read USB Interrupt In.";
        //private const string ERR_9003_MSG = "Invalid Device ID used to Read Data.";
        //private const string ERR_9004_MSG = "Unable to start a session with the USB Device.";
        //private const string ERR_9005_MSG = "Unable to find USB Device Driver.";
        //private const string ERR_9006_MSG = "Unable to get the name of the Device.";
        //private const string ERR_9007_MSG = "Packet Start not found in buffer.";
        //private const string ERR_9008_MSG = "Timed out during a Write Operation.";
        private const string ERR_9009_MSG = "Timed out during a Read Operation.";
        //private const string ERR_3010_MSG = "The argument must be greater than zero.";
        //private const string ERR_3011_MSG = "The argument length must be greater than zero.";
        //private const string ERR_3012_MSG = "CreateFile returned the API Error: ";
        //private const string ERR_3013_MSG = "CloseHandle returned the API Error: ";

		// Garmin SDK constants
		private static Guid GUID_DEVINTERFACE_GRMNUSB = new Guid("2C9C45C2-8E7D-4C08-A12D-816BBAE722C0");
        private static uint IOCTL_USB_PACKET_SIZE = NativeWin32Methods.CTL_CODE(NativeWin32Methods.FILE_DEVICE_UNKNOWN, 0x851, NativeWin32Methods.METHOD_BUFFERED, NativeWin32Methods.FILE_ANY_ACCESS);
        private const int NOSUPPORT = -1;

        private int[] mPIDData = new int[19];//PID Data table Data
        private LinkProtocol miProtocol; //protocol to be used

		private string portName;
		private IntPtr deviceHandle;
		private int usbPacketSize;
		private UsbLinkWorkerThread workerThread;
		private uint unitId;

		public override long UnitId
		{
			get
			{
				return (long)unitId;
			}
		}

		public UsbLink()
		{
			try
			{
                //assume L001 for all usb transfers
                SetPIDValues(LinkProtocol.L001);
			}
			finally
			{
			}
		}

		protected override void Dispose(bool disposing)
		{
			try
			{
				CloseUsbDevice();
				base.Dispose(disposing);
			}
			finally
			{
			}
		}
        /// <summary>
        /// This method was added for backward compatibility. It uses the first usb device detected.
        /// </summary>
        public void PortOpen()
        {
            string[] devices = UsbLink.GetConnectedDeviceNames();
            PortOpen(devices[0]);
        }
        /// <summary>
        /// This method was added for backward compatibility. It uses the first usb device detected.
        /// </summary>
        public void PortOpen(string portName)
        {
            Open(portName);
            StartSession();
        }
        /// <summary>
        /// This method was added for backward compatibility.
        /// </summary>
        public void PortClose()
        {
            EndSession();
        }

		public override void Open(string portName)
		{
			try
			{
				this.portName = portName;
				// Do nothing; we actually open the USB device before each transfer.
			}
			finally
			{
			}
		}

		public override void Close()
		{
			try
			{
				portName = null;
				// Do nothing; we actually close the USB device after each transfer.
			}
			finally
			{
			}
		}

		public override void StartSession()
		{
			try
			{
				OpenUsbDevice(portName);

				workerThread = new UsbLinkWorkerThread(deviceHandle, usbPacketSize, linkPacketIds);
				workerThread.Start();

				ClearBuffer();
				workerThread.TransmitPacket(new UsbLinkPacket(UsbPacketType.UsbProtocolLayer, linkPacketIds.StartSession));
				UsbLinkPacket packet = workerThread.ReceivePacket();
				if (packet.PacketType != UsbPacketType.UsbProtocolLayer || packet.PacketId != linkPacketIds.SessionStarted)
					throw new UsbLinkException("Unable to start a session with the USB device.");

				unitId = packet.GetDataUInt32(0);
                //if (GpsTrace.GpsSwitch.TraceInfo)
                //    GpsTrace.WriteLine("Session established with unit id {0}", unitId);
			}
			catch(Exception ex)
			{
				EndSession();
                throw new Waymex.Gps.USBTransportException(ERR_9000_MSG, ex);
			}
			finally
			{
			}
		}

		public override void EndSession()
		{
			try
			{
				unitId = 0;
				// We close the USB device first, so that any pending I/O operations will be aborted, freeing up the thread.
				CloseUsbDevice();
				if (workerThread != null)
				{
					workerThread.Stop();
					workerThread.Dispose();
					workerThread = null;
				}
			}
			finally
			{
			}
		}

		private static void ThrowNativeErrorException(int error, string apiName)
		{
            //if (GpsTrace.GpsSwitch.TraceError)
            //    GpsTrace.WriteLine("ERROR: {0} failed with error code {1}", apiName, error);
			Win32Exception ex = new Win32Exception(error);
            throw new ApplicationException(String.Format(CultureInfo.CurrentCulture, "An unexpected communication error occurred: {0}", ex.Message), ex);
		}

		private void OpenUsbDevice(string deviceName)
		{
			try
			{
				// To prevent leaks, make sure the device is closed first
				CloseUsbDevice();

				try
				{
#if EMULATE_DEVICE
					deviceHandle = (IntPtr)1;
#else
					deviceHandle = NativeWin32Methods.CreateFile(deviceName,
                        NativeWin32Methods.GENERIC_READ | NativeWin32Methods.GENERIC_WRITE,
                        0, null, NativeWin32Methods.OPEN_EXISTING, 0, IntPtr.Zero);
#endif
                    if (deviceHandle == NativeWin32Methods.INVALID_HANDLE_VALUE)
						ThrowNativeErrorException(Marshal.GetLastWin32Error(), "CreateFile");
#if EMULATE_DEVICE
					usbPacketSize = 64;
#else
					using (NativeBuffer inputBuffer = new NativeBuffer(Marshal.SizeOf(typeof(uint))))
					{
						uint bytesReturned = 0;
                        if (!NativeWin32Methods.DeviceIoControl(deviceHandle, IOCTL_USB_PACKET_SIZE, IntPtr.Zero, 0,
							inputBuffer.Pointer, (uint)inputBuffer.Size, out bytesReturned, null))
							ThrowNativeErrorException(Marshal.GetLastWin32Error(), "DeviceIoControl");
						if (bytesReturned == Marshal.SizeOf(typeof(uint)))
							usbPacketSize = Marshal.ReadInt32(inputBuffer.Pointer);
						else if (bytesReturned == Marshal.SizeOf(typeof(ushort)))
							usbPacketSize = Marshal.ReadInt16(inputBuffer.Pointer);
						else
                            throw new ApplicationException("Did not receive expected data size back when querying USB packet size.");
					}
#endif
                    //if (GpsTrace.GpsSwitch.TraceVerbose)
                    //    GpsTrace.WriteLine("USB packet size={0}", usbPacketSize);
				}
				catch
				{
					CloseUsbDevice();
					throw;
				}
			}
			finally
			{
			}
		}

		private void CloseUsbDevice()
		{
			try
			{
				if (deviceHandle != IntPtr.Zero)
				{
                    //GpsTrace.WriteLineIf(GpsTrace.GpsSwitch.TraceVerbose, "Closing device file");
#if !EMULATE_DEVICE
                    NativeWin32Methods.CloseHandle(deviceHandle);
#endif
					deviceHandle = IntPtr.Zero;
					// The Garmin USB device driver can require a small amount of time to close the device.
					// Pause slightly here so that a subsequent reopen won't fail.
					Thread.Sleep(50);
				}
			}
			finally
			{
			}
		}

		internal static string[] GetConnectedDeviceNames()
		{
			try
			{
				ArrayList deviceNames = new ArrayList();
#if EMULATE_DEVICE
				deviceNames.Add(@"\\?\usb#vid_091e&pid_0003#5&a63e806&0&1#{2c9c45c2-8e7d-4c08-a12d-816bbae722c0}");
#else
                IntPtr deviceInfoSetHandle = NativeWin32Methods.SetupDiGetClassDevs(ref GUID_DEVINTERFACE_GRMNUSB, null, IntPtr.Zero,
                    NativeWin32Methods.DIGCF_PRESENT | NativeWin32Methods.DIGCF_DEVICEINTERFACE);
                if (deviceInfoSetHandle == NativeWin32Methods.INVALID_HANDLE_VALUE)
					ThrowNativeErrorException(Marshal.GetLastWin32Error(), "SetupDiGetClassDevs");
				try
				{
                    NativeWin32Methods.SP_DEVICE_INTERFACE_DATA deviceInterfaceData = new NativeWin32Methods.SP_DEVICE_INTERFACE_DATA();
					int deviceIndex = 0;
					while (true)
					{
						deviceInterfaceData.cbSize = (uint)Marshal.SizeOf(deviceInterfaceData);
                        if (!NativeWin32Methods.SetupDiEnumDeviceInterfaces(deviceInfoSetHandle, null, ref GUID_DEVINTERFACE_GRMNUSB, (uint)deviceIndex, deviceInterfaceData))
						{
							int lastError = Marshal.GetLastWin32Error();
                            if (lastError == NativeWin32Methods.ERROR_NO_MORE_ITEMS)
								break;
							ThrowNativeErrorException(lastError, "SetupDiEnumDeviceInterfaces");
						}

                        NativeWin32Methods.SP_DEVICE_INTERFACE_DETAIL_DATA deviceDetailData = new NativeWin32Methods.SP_DEVICE_INTERFACE_DETAIL_DATA();
                        deviceDetailData.cbSize = (uint)Marshal.SizeOf(typeof(NativeWin32Methods.SP_DEVICE_INTERFACE_DETAIL_DATA_EXACT));
						uint requiredSize = 0;
                        if (!NativeWin32Methods.SetupDiGetDeviceInterfaceDetail(deviceInfoSetHandle, deviceInterfaceData, deviceDetailData, (uint)Marshal.SizeOf(deviceDetailData), out requiredSize, null))
							ThrowNativeErrorException(Marshal.GetLastWin32Error(), "SetupDiGetDeviceInterfaceDetail");
						deviceNames.Add(deviceDetailData.DevicePath);
						++deviceIndex;
					}
				}
				finally
				{
                    NativeWin32Methods.SetupDiDestroyDeviceInfoList(deviceInfoSetHandle);
				}
#endif
                //if (GpsTrace.GpsSwitch.TraceInfo)
                //{
                //    foreach (string deviceName in deviceNames)
                //        GpsTrace.WriteLine("Name={0}", deviceName);
                //}
				return (string[])deviceNames.ToArray(typeof(string));
			}
			finally
			{
			}
		}

		public override void ClearBuffer()
		{
			try
			{
				workerThread.ClearReceivePacketQueue();
			}
			finally
			{
			}
		}
        /// <summary>
        /// This method has been added for backward compatibility.
        /// </summary>
        /// <param name="pidData"></param>
        /// <returns></returns>
        public override bool TransmitData(GpsPidData pidData)
        {
            ApplicationPacket packet;

            //convert PidData to an ApplicationPacket
            byte packetId = (byte)mPIDData[(int)pidData.Pid];

            if(pidData.ByteData == null)
               packet = new ApplicationPacket(packetId);
            else
                packet = new ApplicationPacket(packetId, pidData.ByteData);

            TransmitData(packet, false);
            return true;
        }
		public override void TransmitData(ApplicationPacket packet, bool ackRequired)
		{
            //ackRequired not used on Usb
            try
            {
                workerThread.TransmitPacket(new UsbLinkPacket(packet));
            }
            finally
			{
			}
		}

        public override GpsPidData ReceiveData()
        {
            try
            {
                int maxTries = 5; // This is just to protect against lock-up due to weird data from device
                while (maxTries-- > 0)
                {
                    UsbLinkPacket packet = workerThread.ReceivePacket();
                    if (packet.PacketType == UsbPacketType.ApplicationLayer)
                    {
                        //create GpsPid object to return
                        GpsPidData PData = new GpsPidData();
                        if (packet.DataSize > 0)
                        {
                            PData.Pid = LookupPID((byte)packet.PacketId);
                            PData.ByteData = packet.DataToArray();
                        }
                        else
                        {
                            PData.Pid = ProcessId.Null;
                            PData.ByteData = null;
                        }
                        return PData;
                        
                        //if (packet.DataSize > 0)
                        //    return new ApplicationPacket((byte)packet.PacketId, packet.DataToArray());
                        //else
                        //    return new ApplicationPacket((byte)packet.PacketId);
                    }
                    //else
                    //    GpsTrace.WriteLineIf(GpsTrace.GpsSwitch.TraceWarning, "WARNING: Ignoring non-application layer packet");
                }
                throw new UsbLinkTimeoutException(ERR_9009_MSG);
            }
            finally
            {
            }
        }
        private void SetPIDValues(LinkProtocol Protocol)
        {
            //---------------------------------------------------------------------------
            //  DESCRIPTION:	This populates the Process ID array (mPIDData). This
            //                  is used throughout the module to translate the enumerated
            //                  PID value passed as arguments to the actual value required
            //                  depending on the protocol selected. This Procedure
            //                  is called when this class initialises to set default
            //                  values.
            //            
            //	PARAMETERS:
            //                  Protocol    Enumerated Integer representing L000,
            //                              L001 or L002.
            //    
            //  RETURNS:
            //                  <None>
            // 
            //---------------------------------------------------------------------------
            //

            try
            {
                //set the common L000/1/2 protocol values
                mPIDData[0] = 6; //ACK_Byte
                mPIDData[1] = 21; //NAK_Byte
                mPIDData[2] = 253; //Protocol_Array
                mPIDData[3] = 254; //Product_Rqst
                mPIDData[4] = 255; //Product_Data

                //set the rest to -1
                for (int f = 5; f <= 18; f++)
                {
                    mPIDData[f] = NOSUPPORT;
                }

                //determine the additional protocol values to use
                switch (Protocol)
                {
                    //
                    case LinkProtocol.L001://basic values plus these
                        {

                            mPIDData[5] = 10; //Command_Data
                            mPIDData[6] = 12; //Xfer_Cmplt
                            mPIDData[7] = 14; //Date_Time
                            mPIDData[8] = 17; //Position_Data
                            mPIDData[9] = 19; //Prx_Wpt_Data
                            mPIDData[10] = 27; //Records
                            mPIDData[11] = 29; //Rte_Hdr
                            mPIDData[12] = 30; //Rte_Wpt_Data
                            mPIDData[13] = 31; //Almanac_Data
                            mPIDData[14] = 34; //Trk_Data
                            mPIDData[15] = 35; //Wpt_Data
                            mPIDData[16] = 51; //Pvt_Data
                            mPIDData[17] = 98; //Rte_Link_Data
                            mPIDData[18] = 99; //Trk_Hdr

                            break;
                        }
                    case LinkProtocol.L002: //basic values plus these
                        {
                            mPIDData[5] = 11; //Command_Data
                            mPIDData[6] = 12; //Xfer_Cmplt
                            mPIDData[7] = 20; //Date_Time
                            mPIDData[8] = 24; //Position_Data
                            mPIDData[9] = NOSUPPORT; //Prx_Wpt_Data (not supported)
                            mPIDData[10] = 35; //Records
                            mPIDData[11] = 37; //Rte_Hdr
                            mPIDData[12] = 39; //Rte_Wpt_Data
                            mPIDData[13] = 4; //Almanac_Data
                            mPIDData[15] = 43; //Wpt_Data
                            mPIDData[16] = NOSUPPORT; //Pvt_Data (not supported)
                            mPIDData[17] = NOSUPPORT; //Rte_Link_Data (not supported)
                            mPIDData[18] = NOSUPPORT; //Trk_Hdr (not supported)

                            break;
                        }
                }
            }
            catch
            {
                throw;
            }

        }
        private ProcessId LookupPID(byte bPID)
        {
            //---------------------------------------------------------------------------
            //   DESCRIPTION:    This function converts a PID to the enumerated
            //                   equivelent. Internally (i.e. when passing PID's between
            //                   application layer and link layer PIDs are represented as
            //                   enumerated integer. This is then converted to the actual
            //                   PID value in the link layer depending upon the protocol
            //                   property. This function does the reverse by returning
            //                   the integer value of the PID based on the actual PID
            //                   value passed and the protocol curently set.
            //
            //   PARAMETERS:
            //                   bPID        Byte containing actual PID
            //
            //   RETURNS:
            //                   ProcessId   Enumerated integer representing the internal
            //                               protocol independent PID value. If the
            //                               PID cannot be found, -1 is returned
            //
            //---------------------------------------------------------------------------
            //
            //process array should always be populated with valid data
            //int iPID = 0;

            ProcessId Pid = new ProcessId();

            //run through module level array looking for first occurence of actual PID
            for (int f = 0; f < mPIDData.Length; f++)
            {
                if (mPIDData[f] == (int)bPID)
                {
                    switch (f)
                    {
                        case 0:
                            Pid = ProcessId.AckByte;
                            break;
                        case 1:
                            Pid = ProcessId.NakByte;
                            break;
                        case 2:
                            Pid = ProcessId.ProtocolArray;
                            break;
                        case 3:
                            Pid = ProcessId.ProductRequest;
                            break;
                        case 4:
                            Pid = ProcessId.ProductData;
                            break;
                        case 5:
                            Pid = ProcessId.CommandData;
                            break;
                        case 6:
                            Pid = ProcessId.TransferComplete;
                            break;
                        case 7:
                            Pid = ProcessId.DateTimeData;
                            break;
                        case 8:
                            Pid = ProcessId.PositionData;
                            break;
                        case 9:
                            Pid = ProcessId.ProximityWaypointData;
                            break;
                        case 10:
                            Pid = ProcessId.Records;
                            break;
                        case 11:
                            Pid = ProcessId.RouteHeader;
                            break;
                        case 12:
                            Pid = ProcessId.RouteWaypointData;
                            break;
                        case 13:
                            Pid = ProcessId.AlmanacData;
                            break;
                        case 14:
                            Pid = ProcessId.TrackData;
                            break;
                        case 15:
                            Pid = ProcessId.WaypointData;
                            break;
                        case 16:
                            Pid = ProcessId.PvtData;
                            break;
                        case 17:
                            Pid = ProcessId.RouteLinkData;
                            break;
                        case 18:
                            Pid = ProcessId.TrackHeader;
                            break;
                        case 19:
                            Pid = ProcessId.FlightBookRecord;
                            break;
                        case 20:
                            Pid = ProcessId.LapData;
                            break;
                        case 255:
                            Pid = ProcessId.Null;
                            break;
                        default:
                            //Un Documented PID;
                            break;
                    }
                    break;
                }
                else
                {
                    Pid = ProcessId.Null;
                }
            }
            return Pid;
        }
        internal LinkProtocol Protocol
        {
            //---------------------------------------------------------------------------
            //   DESCRIPTION:    Sets the module level variable to indicate the required
            //                   link protocol and calls the setPIDValues procedure to
            //                   set the actual pid values for the protocol selected.
            //
            //
            //   PARAMETERS:     iProt   Enumerated integer indicating the required link
            //                           level protocol, L000, L001 or L002.
            //
            //
            //   RETURNS:        <none>
            //
            //
            //---------------------------------------------------------------------------

            set
            {
                try
                {
                    //save the value for the get property
                    miProtocol = value;

                    //use a function to set the actual values so that the same function can
                    //be called in the initialise event with a default value
                    SetPIDValues(miProtocol);
                }
                catch
                {
                    throw;
                }
            }
        }
	}
}
